4-5 RBAC角色权限实现:用户角色CURD操作
概述
本节使用 NestJS CLI 快速生成 Role 模块的完整 CRUD 结构,并实现角色的创建、查询、更新、删除接口。同时介绍 Prisma 的分页查询方法和 DTO 校验配置。
使用 NestJS CLI 快速生成模块
NestJS CLI 提供了快捷命令,一次性生成完整的 REST API 模块:
nest g resource role --no-spec
bash
CLI 会提供选项:
- 选择 REST API 模式
- 是否生成 CRUD 入口文件(选择 Yes)
生成的文件结构:
src/role/
├── dto/
│ ├── create-role.dto.ts
│ └── update-role.dto.ts
├── entities/
│ └── role.entity.ts # 使用 Prisma 时不需要
├── role.controller.ts
├── role.module.ts
└── role.service.ts
text
注意:
entities/目录在使用 Prisma 时不需要,可以删除。Prisma 的模型定义在schema.prisma中。
Prisma Client 注入方式
方式一:直接注入 PrismaClient(推荐用于单数据库)
// role.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; // 从 schema 生成的类型
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
@Injectable()
export class RoleService {
constructor(private prismaClient: PrismaClient) {}
async create(createRoleDto: CreateRoleDto) {
return this.prismaClient.role.create({
data: createRoleDto,
});
}
}
typescript
方式二:通过模块提供 PrismaClient
// role.module.ts
import { Module } from '@nestjs/common';
import { RoleService } from './role.service';
import { RoleController } from './role.controller';
import { PrismaClient } from '@prisma/client';
@Module({
controllers: [RoleController],
providers: [RoleService, PrismaClient], // 注册 PrismaClient
exports: [RoleService], // 导出以供其他模块使用
})
export class RoleModule {}
typescript
PrismaClient 导入路径
// 从 schema 生成的客户端类型导入(推荐)
import { PrismaClient } from '@prisma/client';
// 也可以从 schema 输出目录导入(自定义输出路径时)
import { PrismaClient } from '../../../prisma/generated';
typescript
DTO 定义与校验
// dto/create-role.dto.ts
import { IsNotEmpty, IsString, IsOptional } from 'class-validator';
export class CreateRoleDto {
@IsNotEmpty({ message: '角色名称不能为空' })
@IsString({ message: '角色名称必须是字符串' })
name: string;
@IsOptional()
@IsString({ message: '描述必须是字符串' })
description?: string;
}
typescript
// dto/update-role.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateRoleDto } from './create-role.dto';
export class UpdateRoleDto extends PartialType(CreateRoleDto) {}
typescript
完整 CRUD 实现
RoleService
// role.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
@Injectable()
export class RoleService {
constructor(private prismaClient: PrismaClient) {}
// 创建角色
async create(createRoleDto: CreateRoleDto) {
return this.prismaClient.role.create({
data: createRoleDto,
});
}
// 查询所有角色(分页)
async findAll(page: number = 1, limit: number = 10) {
const skip = (page - 1) * limit;
return this.prismaClient.role.findMany({
skip,
take: limit,
});
}
// 查询单个角色
async findOne(id: number) {
return this.prismaClient.role.findUnique({
where: { id },
});
}
// 更新角色
async update(id: number, updateRoleDto: UpdateRoleDto) {
return this.prismaClient.role.update({
where: { id },
data: updateRoleDto,
});
}
// 删除角色
async remove(id: number) {
return this.prismaClient.role.delete({
where: { id },
});
}
// 根据角色 ID 列表查询(后续 Guard 中使用)
async findByIds(ids: number[]) {
return this.prismaClient.role.findMany({
where: { id: { in: ids } },
include: {
rolePermissions: {
include: {
permission: true,
},
},
},
});
}
}
typescript
RoleController
// role.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
ParseIntPipe,
} from '@nestjs/common';
import { RoleService } from './role.service';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
@Controller('role')
export class RoleController {
constructor(private readonly roleService: RoleService) {}
@Post()
create(@Body() createRoleDto: CreateRoleDto) {
return this.roleService.create(createRoleDto);
}
@Get()
findAll(
@Query('page', new ParseIntPipe({ optional: true })) page: number = 1,
@Query('limit', new ParseIntPipe({ optional: true })) limit: number = 10,
) {
return this.roleService.findAll(page, limit);
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.roleService.findOne(id);
}
@Put(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() updateRoleDto: UpdateRoleDto) {
return this.roleService.update(id, updateRoleDto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.roleService.remove(id);
}
}
typescript
Prisma 分页查询
Prisma 使用 skip 和 take 实现分页:
const skip = (page - 1) * limit;
const results = await prismaClient.role.findMany({
skip, // 跳过前 N 条记录
take: limit, // 取 N 条记录
});
typescript
| 参数 | 说明 | 示例(page=2, limit=10) |
|---|---|---|
skip | 跳过的记录数 | (2-1) * 10 = 10 |
take | 获取的记录数 | 10 |
ParseIntPipe 可选参数
使用 optional: true 使分页参数可选,并在 Controller 中设置默认值:
@Query('page', new ParseIntPipe({ optional: true })) page: number = 1
typescript
注意:ParseIntPipe 不能直接设置默认值,默认值需要在参数声明中通过 = 1 的方式设置。
测试接口示例
| 操作 | 方法 | 路径 | Body / 参数 |
|---|---|---|---|
| 创建角色 | POST | /role | { "name": "超级管理员", "description": "平台所有权限" } |
| 获取角色列表 | GET | /role?page=1&limit=10 | - |
| 获取单个角色 | GET | /role/2 | - |
| 更新角色 | PUT | /role/2 | { "name": "管理员", "description": "除权限设置外所有功能" } |
| 删除角色 | DELETE | /role/1 | - |
常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 404 Not Found | RoleModule 未注册到 AppModule | 在 AppModule 的 imports 中添加 RoleModule |
undefined 响应 | 全局路由冲突(如 AppController 捕获了所有 POST) | 检查并清理 AppController 中的通配路由 |
| Prisma 类型报错 | Client 未重新生成 | 执行 prisma generate |
模块注册
确保 RoleModule 已注册到 AppModule:
// app.module.ts
import { Module } from '@nestjs/common';
import { RoleModule } from './role/role.module';
@Module({
imports: [RoleModule, /* ... */],
})
export class AppModule {}
typescript
↑